package com.phoebe.auth.interceptor;

import com.common.annotation.RequireAuth;
import com.common.consts.AuthConsts;
import com.common.consts.AuthHeaders;
import com.common.consts.CommonRespCode;
import com.common.consts.RedisConsts;
import com.common.exception.AuthException;
import com.common.util.SignUtil;
import com.phoebe.auth.model.AuthAccessKey;
import com.phoebe.auth.model.AuthApp;
import com.phoebe.auth.service.AuthAccessService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author: dean
 * @email: zhaojd1604@gmail.com
 * @date: 2018/12/29
 * @description: 开放API拦截器
 */
@Slf4j
@Component
public class ApiAuthInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private AuthAccessService authAccessService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 增加跨域设置
        allowCors(response);
        if (validateAuth(request, handler)) {
            return true;
        }
        throw new AuthException(CommonRespCode.NO_AUTH);
    }

    /**
     * 设置允许跨域
     *
     * @param response HttpResponse
     */
    private void allowCors(HttpServletResponse response) {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    }

    /**
     * 是否拥有调用权限
     *
     * @param handler
     * @return
     */
    private boolean validateAuth(HttpServletRequest request, Object handler) throws AuthException {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod method = (HandlerMethod) handler;
        // 是否需要权限校验
        if (!needValidateAuth(method)) {
            return true;
        }

        String appKey = request.getHeader(AuthHeaders.PHOEBE_APP_KEY.getHeaderName());
        String accessKeyId = request.getHeader(AuthHeaders.PHOEBE_ACCESS_KEY_ID.getHeaderName());
        Long timestamp = Long.valueOf(request.getHeader(AuthHeaders.PHOEBE_TIMESTAMP.getHeaderName()));
        String nonce = request.getHeader(AuthHeaders.PHOEBE_NONCE.getHeaderName());
        String authorization = request.getHeader(AuthHeaders.AUTHORIZATION.getHeaderName());

        // 判断时间戳
        Long now = System.currentTimeMillis();
        if (timestamp + AuthConsts.AUTH_THRESHOLD_MILLIS <= now) {
            log.info("signature invalid,current timestamp:{},request timestamp:{}", now, timestamp);
            throw new AuthException(CommonRespCode.SIGN_EXPIRED);
        }

        // 判断随机数唯一性
        if (!checkNonceUnique(appKey, nonce)) {
            throw new AuthException(CommonRespCode.NONSE_IS_USED);
        }

        AuthApp app = authAccessService.findByKey(appKey);
        if (null == app) {
            log.info("application unauthorized,AppKey:", appKey);
            throw new AuthException(CommonRespCode.APP_NOT_IN_AUTH);
        }
        AuthAccessKey accessKey = authAccessService.findByAppIdAndAccessKeyId(app.getId(), accessKeyId);
        if (null == accessKey) {
            log.info("AccessKey invalid,AccessKeyId:{}", accessKeyId);
            throw new AuthException(CommonRespCode.ACCESS_KEY_INVALIDATED);
        }
        // 处理签名参数
        Map<String, String> params = processSignParamMap(request, appKey, accessKeyId, timestamp, nonce);
        // 签名
        String sign = SignUtil.sign(request.getMethod(), params, accessKey.getAccessKeySecret());
        // 判断签名值
        if (sign.equals(authorization.trim())) {
            return true;
        } else {
            log.info("data signature error,client signature：{}，server signature：{}", authorization, sign);
        }
        return false;
    }

    /**
     * 处理待签名参数
     *
     * @param request
     * @param appKey
     * @param accessKeyId
     * @param timestamp
     * @param nonce
     * @return
     */
    private Map<String, String> processSignParamMap(HttpServletRequest request, String appKey, String accessKeyId,
                                                    Long timestamp, String nonce) {
        Map<String, String> params = new HashMap<>();
        // 请求参数
        Map<String, String[]> queryParams = request.getParameterMap();
        queryParams.forEach((key, value) -> params.put(key, Arrays.toString(value)));
        // header参数
        params.put(AuthHeaders.PHOEBE_APP_KEY.getHeaderName(), appKey);
        params.put(AuthHeaders.PHOEBE_ACCESS_KEY_ID.getHeaderName(), accessKeyId);
        params.put(AuthHeaders.PHOEBE_TIMESTAMP.getHeaderName(), String.valueOf(timestamp));
        params.put(AuthHeaders.PHOEBE_NONCE.getHeaderName(), nonce);
        return params;
    }

    /**
     * 判断是否需要校验权限
     *
     * @param method method
     * @return is need validate boolean
     */
    private boolean needValidateAuth(HandlerMethod method) {
        if (method.getMethodAnnotation(RequireAuth.class) == null) {
            if (method.getBeanType().getAnnotation(RequireAuth.class) == null) {
                return false;
            }
        }
        return true;
    }

    /*
     * 校验随机数唯一性
     */
    private boolean checkNonceUnique(String appKey, String noise) {
        String redisKey = RedisConsts.AUTH_NOISE + RedisConsts.SEPARATOR + appKey + RedisConsts.SEPARATOR + noise;

        Object value = redisTemplate.opsForValue().get(redisKey);
        if (null != value) {
            return false;
        }
        redisTemplate.opsForValue().set(redisKey, 1, RedisConsts.DEFAULT_EXPIRED_TIME, TimeUnit.SECONDS);
        return true;
    }
}